<template>
  <component ref="formEl" :is="formRegister ? 'w-form-element' : 'div'" v-bind="
      formRegister && {
        validators,
        inputValue: rangeValueScaled,
        disabled,
        readonly
      }"
      v-model:valid="valid" @reset="rangeValuePercent = 0;updateRangeValueScaled();"
      :class="wrapperClasses">
      <label class="w-slider__label w-slider__label--left w-form-el-shakable"
        v-if="$slots['label-left']" :for="`button--${_.uid}`">
      <slot name="label-left"></slot>
      </label>
      <label class="w-slider__label w-slider__label--left w-form-el-shakable" v-else-if="labelLeft"
        :for="`button--${_.uid}`" v-html="labelLeft">
      </label>
    <div class="w-slider__track-wrap">
      <div class="w-slider__track" ref="trackRef" @mousedown="onTrackMouseDown" @touchstart="onTrackMouseDown"
        :class="trackClasses" role="slider" aria-label="Slider" :aria-valuemin="minVal" :aria-valuemax="maxVal"
        :aria-valuenow="rangeValueScaled" :aria-readonly="readonly ? 'true' : 'false'" aria-orientation="horizontal">
        <div class="w-slider__range" :class="rangeClasses" :style="rangeStyles"></div>
        <div class="w-slider__thumb" :style="thumbStyles">
          <button class="w-slider__thumb-button" ref="thumbRef" :id="`button--${_.uid}`" :class="[color]"
            :name="inputName" :model-value="rangeValueScaled" :disabled="disabled || null" :readonly="readonly || null"
            :aria-readonly="readonly ? 'true' : 'false'" @keydown.left="onKeyDown($event, -1)"
            @keydown.right="onKeyDown($event, 1)" @focus="$emit('focus', $event)" @click.prevent></button><label
            class="w-slider__thumb-label" v-if="thumbLabel" :for="`button--${_.uid}`" :class="thumbClasses">
            <div v-if="thumbLabel === 'droplet'">
              <slot name="label" :value="rangeValueScaled">{{
                ~~rangeValueScaled
                }}</slot>
            </div>
            <slot v-else name="label" :value="rangeValueScaled">{{
              ~~rangeValueScaled
              }}</slot>
          </label>
        </div>
      </div>
      <div class="w-slider__step-labels" v-if="stepLabels && step">
        <div class="w-slider__step-label" @click="onStepLabelClick(0)">
          {{ this.minVal }}
        </div>
        <div class="w-slider__step-label" v-for="currStep in ~~numberOfSteps" :key="currStep"
          @click="onStepLabelClick(currStep * (100 / numberOfSteps))"
          :style="`left: ${currStep * (100 / numberOfSteps)}%`">
          {{ percentToScaled(currStep * (100 / numberOfSteps)) }}
        </div>
        <div class="w-slider__step-label" v-if="~~numberOfSteps !== numberOfSteps" @click="onStepLabelClick(100)"
          style="left: 100%">
          {{ this.maxVal }}
        </div>
      </div>
    </div>
    <label class="w-slider__label w-slider__label--right w-form-el-shakable" v-if="$slots['label-right']"
      :for="`button--${_.uid}`">
      <slot name="label-right"></slot>
    </label>
    <label class="w-slider__label w-slider__label--right w-form-el-shakable" v-else-if="labelRight"
      :for="`button--${_.uid}`" v-html="labelRight">
    </label>
  </component>
</template>

<script>
  import {
    ref,
    reactive,
    computed,
    watch,
    onMounted,
    onBeforeMount,
    nextTick,
    getCurrentInstance
  } from "vue";
  export default {
    name: "w-slider",
    inject: { formRegister: { default: null } },
    props: {
      modelValue: { type: Number, default: 0 },
      color: { type: String, default: "primary" },
      bgColor: { type: String },
      stepLabels: { type: [Boolean, Array] },
      thumbLabel: { type: [Boolean, String] }, // One of true, false, 'droplet'.
      thumbLabelClass: { type: String },
      trackClass: { type: String },
      rangeClass: { type: String },
      min: { type: [Number, String], default: 0 },
      max: { type: [Number, String], default: 100 },
      step: { type: [Number, String] },
      labelLeft: { type: String },
      labelRight: { type: String },
      // Also name, disabled, readonly, required and validators in the mixin.
      name: { type: String }, // When sending data through form.
      disabled: { type: Boolean },
      readonly: { type: Boolean },
      required: { type: Boolean },
      validators: { type: Array }
    },

    emits: ["input", "update:modelValue", "focus"],

    setup(props, ctx) {
      const instance = getCurrentInstance();
      const track = reactive({
        el: null,
        left: 0,
        width: 0
      });
      const dragging = ref(false);
      const rangeValuePercent = ref(0);
      const rangeValueScaled = ref(0);
      const valid = ref(null);
      // refs
      const thumbRef = ref(null);
      const trackRef = ref(null);

      const minVal = computed(() => {
        return parseFloat(props.min);
      });
      const maxVal = computed(() => {
        return parseFloat(props.max);
      });
      const stepValPercent = computed(() => {
        // Don't allow a step that is bigger than the whole scale.
        return (
          (Math.min(parseFloat(props.step), scaledRange.value) /
            scaledRange.value) *
          100
        );
      });
      // Lighten the maths while dragging by caching some of the maths - it's already that!
      const scaledRange = computed(() => {
        return maxVal.value - minVal.value;
      });
      const numberOfSteps = computed(() => {
        return 100 / stepValPercent.value;
      });
      const rangeStyles = computed(() => {
        return {
          width: `${rangeValuePercent.value}%`
        };
      });
      const thumbStyles = computed(() => {
        return {
          left: `${rangeValuePercent.value}%`
        };
      });
      const rangeClasses = computed(() => {
        return {
          [`${props.color}--bg`]: props.color,
          [props.rangeClass]: props.rangeClass || null
        };
      });
      const trackClasses = computed(() => {
        return {
          [`${props.bgColor}--bg`]: props.bgColor,
          [props.trackClass]: props.trackClass || null
        };
      });
      const thumbClasses = computed(() => {
        return {
          [props.thumbLabelClass]: props.thumbLabelClass || null,
          "w-slider__thumb-label--droplet": props.thumbLabel === "droplet"
        };
      });
      const wrapperClasses = computed(() => {
        return {
          "w-slider": true,
          "w-slider--dragging": dragging.value,
          "w-slider--disabled": props.disabled,
          "w-slider--readonly": props.readonly,
          "w-slider--has-step-labels": props.step && props.stepLabels
        };
      });
      const inputName = computed(() => {
        return (
          props.name || `${instance.type.name}--${instance.type.uid || instance.uid}`
        );
      });
      function scaledToPercent(value) {
        // percentage = (value - min) / (max - min)
        return Math.max(
          0,
          Math.min(((value - minVal.value) / scaledRange.value) * 100, 100)
        );
      }

      function percentToScaled(value) {
        return (
          Math.round(((value / 100) * scaledRange.value + minVal.value) * 100) /
          100
        );
      }

      function onTrackMouseDown(e) {
        if (props.disabled || props.readonly) return;
        // On touch screen don't listen for both touchstart & mousedown.
        if ("ontouchstart" in window && e.type === "mousedown") {
          return;
        }

        const { left, width } = track.el.getBoundingClientRect();
        track.width = width;
        track.left = left;
        dragging.value = true;

        updateRange(e.type === "touchstart" ? e.touches[0].clientX : e.clientX);

        document.addEventListener(
          e.type === "touchstart" ? "touchmove" : "mousemove",
          onDrag
        );
        document.addEventListener(
          e.type === "touchstart" ? "touchend" : "mouseup",
          onMouseUp,
          { once: true }
        );
      }

      function onDrag(e) {
        updateRange(e.type === "touchmove" ? e.touches[0].clientX : e.clientX);
      }

      function onMouseUp(e) {
        dragging.value = false;
        document.removeEventListener(
          e.type === "touchend" ? "touchmove" : "mousemove",
          onDrag
        );
        if (thumbRef.value) {
          thumbRef.value.focus();
        }
      }

      function onStepLabelClick(step) {
        rangeValuePercent.value = step;
        updateRangeValueScaled();
      }

      function onKeyDown(e, direction) {
        if (props.disabled || props.readonly) return;

        rangeValuePercent.value +=
          direction * (e.shiftKey ? 5 : 1) * (stepValPercent.value || 1);
        rangeValuePercent.value = Math.max(
          0,
          Math.min(rangeValuePercent.value, 100)
        );
        updateRangeValueScaled();
      }

      function updateRange(cursorPositionX) {
        rangeValuePercent.value = Math.max(
          0,
          Math.min(((cursorPositionX - track.left) / track.width) * 100, 100)
        );
        if (props.step) {
          const valuePlusHalfStep =
            rangeValuePercent.value + stepValPercent.value / 2;
          rangeValuePercent.value =
            valuePlusHalfStep - (valuePlusHalfStep % stepValPercent.value);
        }
        updateRangeValueScaled();
      }

      function updateRangeValueScaled() {
        rangeValueScaled.value = percentToScaled(rangeValuePercent.value);
        ctx.emit("update:modelValue", rangeValueScaled.value);
        ctx.emit("input", rangeValueScaled.value);
      }

      onBeforeMount(() => {
        nextTick(() => {
          track.el = trackRef.value;
          rangeValueScaled.value = props.modelValue;
          rangeValuePercent.value = scaledToPercent(props.modelValue);
        });
      });

      watch(
        () => props.modelValue,
        value => {
          if (rangeValueScaled.value !== value) {
            rangeValueScaled.value = value;
            rangeValuePercent.value = scaledToPercent(value);
          }
        }
      );

      return {
        track,
        dragging,
        rangeValuePercent,
        rangeValueScaled,
        valid,

        // refs
        thumbRef,
        trackRef,
        // computed
        minVal,
        maxVal,
        stepValPercent,
        scaledRange,
        numberOfSteps,
        rangeStyles,
        thumbStyles,
        rangeClasses,
        trackClasses,
        thumbClasses,
        wrapperClasses,
        inputName,
        // methods
        scaledToPercent,
        percentToScaled,
        onTrackMouseDown,
        onDrag,
        onMouseUp,
        onStepLabelClick,
        onKeyDown,
        updateRange,
        updateRangeValueScaled
      };
    }
  };
</script>